package org.altervista.cp.microkernel.persistence.spi;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.altervista.cp.microkernel.log.Log;
import org.altervista.cp.microkernel.persistence.PersistenceException;
import org.altervista.cp.microkernel.persistence.PersistenceQuery;
import org.altervista.cp.microkernel.persistence.PersistenceSession;

public class DefaultPersistenceSession implements PersistenceSession {

	private static final Log log = new Log(DefaultPersistenceSession.class);

	public void close() {
		for (PreparedStatement ps : _localQueries.get()) {
			try {
				ps.close();
			} catch (SQLException e) {
				log.error("Unexpected error", e);
			}
		}
		rollback();
		try {
			_c.close();
		} catch (SQLException e) {
			throw new PersistenceException(e);
		} finally {
			_c = null;
		}
	}

	public void commit() {
		if (_transactional) {
			try {
				_c.commit();
			} catch (SQLException e) {
				throw new PersistenceException(e);
			}
		}
	}

	public void rollback() {
		if (_transactional) {
			try {
				_c.rollback();
				_c.setAutoCommit(true);
			} catch (SQLException e) {
				throw new PersistenceException(e);
			}
		}
	}

	public <T> T load(Class<T> c, Object... id) {
		try {
			return Persister.MAP.get(c).select(_c, id);
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	public <T> T[] loadAll(Class<T> c) {
		try {
			return Persister.MAP.get(c).selectAll(_c);
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	@SuppressWarnings("unchecked")
	public <T> PersistenceQuery<T> createQuery(final Class<T> c,
			Object... queryDef) {
		if (queryDef.length > 2) {
			throw new IllegalArgumentException("Too many arguments");
		}
		if (queryDef.length == 0 || !(queryDef[0] instanceof String)) {
			throw new IllegalArgumentException(
					"Missing sql query string argument");
		}
		if (queryDef.length == 1) {
			return createQuery(c, (String) queryDef[0]);
		}
		if (queryDef.length == 2 && !(queryDef[1] instanceof Loader)) {
			throw new IllegalArgumentException(
					"Invalid second argument: expected a "
							+ Loader.class.getName() + " instance");
		}
		return createQuery(c, (String) queryDef[0], (Loader) queryDef[1]);
	}

	public <T> PersistenceQuery<T> createQuery(Class<T> c, String sql) {
		return createQuery(c, sql, new DefaultLoader<T>(c));
	}

	public <T> PersistenceQuery<T> createQuery(final Class<T> c,
			final String sql, final Loader<T> loader) {
		try {
			final HashMap<String, Integer> namedParametersMap = new HashMap<String, Integer>();
			StringBuffer sqlBuf = new StringBuffer();
			Matcher m = Pattern.compile(":\\{\\w+}").matcher(sql);
			int i = 0;
			while (m.find()) {
				String group = m.group();
				namedParametersMap.put(group.substring(2, group.length() - 1),
						i++);
				m.appendReplacement(sqlBuf, "?");
			}
			m.appendTail(sqlBuf);
			final PreparedStatement ps = _c.prepareStatement(sqlBuf.toString());
			_localQueries.get().add(ps);

			return new PersistenceQuery<T>() {
				public Results<T> getResults(final boolean connected) {
					return new Results<T>() {
						private ResultSet _rs;
						private boolean _connected;
						private LinkedList<T> _list;

						{
							try {
								_rs = ps.executeQuery();
							} catch (Exception e) {
								throw new PersistenceException(e);
							}
							_connected = connected;
						}

						public Iterator<T> iterator() {
							Iterator<T> it = new Iterator<T>() {
								public boolean hasNext() {
									try {
										return next == null ? (next = _rs
												.next()) : next;
									} catch (SQLException e) {
										throw new PersistenceException(e);
									} finally {
										if (Boolean.FALSE.equals(next)) {
											try {
												_rs.close();
											} catch (SQLException e) {
											}
										}
									}
								}

								public T next() {
									try {
										hasNext();
										T o = c.newInstance();
										loader.set(o, _rs);
										next = null;
										return o;
									} catch (Exception e) {
										throw new PersistenceException(e);
									}
								}

								public void remove() {
									throw new UnsupportedOperationException();
								}

								private Boolean next;
							};

							return connected ? it : asList(it).iterator();
						}

						public List<T> asList() {
							return asList(iterator());
						}

						public boolean isConnected() {
							return _connected;
						}

						private List<T> asList(Iterator<T> it) {
							if (_list == null) {
								_list = new LinkedList<T>();
								while (it.hasNext()) {
									_list.add(it.next());
								}
							}
							return _list;
						}

					};
				}

				public T getUniqueResult() {
					setMaxResults(1);
					Iterator<T> it = getResults(false).iterator();
					return it.hasNext() ? it.next() : null;
				}

				public void setMaxResults(int maxResults) {
					try {
						ps.setMaxRows(maxResults);
					} catch (SQLException e) {
						throw new PersistenceException(e);
					}
				}

				public void setParameter(int index, Object... param) {
					try {
						if (param[0] == null) {
							switch (param.length) {
							case 1:
								ps.setNull(index + 1, Types.VARCHAR);
								break;
							case 2:
								ps.setNull(index + 1, ((Number) param[1])
										.intValue());
								break;
							}
						} else {
							JDBCType jdbcType = new JDBCType(param[0]
									.getClass());
							switch (param.length) {
							case 1:
								ps.setObject(index + 1, jdbcType
										.parse(param[0]), jdbcType.getType());
								break;
							case 2:
								ps.setObject(index + 1, jdbcType
										.parse(param[0]), jdbcType.getType(),
										((Number) param[1]).intValue());
								break;
							}
						}
					} catch (SQLException e) {
						throw new PersistenceException(e);
					}
				}

				public void setParameter(String name, Object... param) {
					if (!namedParametersMap.containsKey(name)) {
						throw new PersistenceException("Invalid parameter: "
								+ name);
					}
					setParameter(namedParametersMap.get(name), param);
				}

			};
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	public void remove(Object o) {
		try {
			Persister.MAP.get(o.getClass()).delete(_c, o);
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	public void insert(Object o) {
		try {
			Persister.MAP.get(o.getClass()).insert(_c, o);
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	public void update(Object o) {
		try {
			Persister.MAP.get(o.getClass()).update(_c, o);
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	public DefaultPersistenceSession(Connection c) {
		try {
			_c = c;
			if (_c.getAutoCommit()) {
				_c.setAutoCommit(false);
				_transactional = true;
			}
		} catch (SQLException e) {
			throw new PersistenceException(e);
		}
	}

	private Connection _c;
	private boolean _transactional;

	private static final ThreadLocal<ArrayList<PreparedStatement>> _localQueries = new ThreadLocal<ArrayList<PreparedStatement>>() {
		protected ArrayList<PreparedStatement> initialValue() {
			return new ArrayList<PreparedStatement>();
		}
	};

}
